/**
 * This file is part of the DbUnit module for NetBeans IDE
 * Copyright (C) 2009 Pavol Pitoňák
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 */

package cz.muni.fi.dbunit;

import cz.muni.fi.dbunit.tasks.AddExtendsTask;
import cz.muni.fi.dbunit.tasks.AddImportTask;
import cz.muni.fi.dbunit.tasks.CloseConnectionTask;
import cz.muni.fi.dbunit.tasks.GetConnectionTask;
import cz.muni.fi.dbunit.tasks.GetDataSetTask;
import cz.muni.fi.dbunit.tasks.GetDatabaseOperationTask;
import cz.muni.fi.dbunit.tasks.GetDatabaseTesterTask;
import cz.muni.fi.dbunit.tasks.NewDatabaseTesterTask;
import java.awt.Component;
import java.awt.Dialog;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.swing.JComponent;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.project.JavaProjectConstants;
import org.netbeans.api.java.project.classpath.ProjectClassPathModifier;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectManager;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.SourceGroup;
import org.netbeans.api.project.Sources;
import org.netbeans.api.project.libraries.Library;
import org.netbeans.api.project.libraries.LibraryManager;
import org.netbeans.modules.junit.JUnitPluginTrampoline;
import org.netbeans.modules.junit.TestUtil;
import org.netbeans.modules.junit.plugin.JUnitPlugin;
import org.netbeans.modules.junit.plugin.JUnitPlugin.CreateTestParam;
import org.openide.DialogDisplayer;
import org.openide.WizardDescriptor;
import org.openide.awt.StatusDisplayer;
import org.openide.cookies.OpenCookie;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataFolder;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.nodes.Node;
import org.openide.util.Exceptions;
import org.openide.util.HelpCtx;
import org.openide.util.NbBundle;
import org.openide.util.actions.NodeAction;

/**
 *
 * @author Pavol Pitoňák 
 */
public final class CreateTestAction extends NodeAction {

    private WizardDescriptor wizard;
    private Project project;
    private Node[] activatedNodes;

    /**
     * Initialize panels representing individual wizard's steps and sets
     * various properties for them influencing wizard appearance.
     */
    private WizardDescriptor.Panel[] getPanels() {
        boolean moreClasses = activatedNodes.length > 1;
        String className = null;
        // if more classes were selected, a special string will be displayed,
        // e.g. 'More than one class selected...', otherwise class name will be displayed
        if (!moreClasses) {
            className = buildClassName(activatedNodes[0]).replace(".java", "");
        }

        WizardDescriptor.Panel[] panels = new WizardDescriptor.Panel[]{
            new SimpleTestWizardPanel(project, className),
            new DbUnitWizardPanel(project)
        };

        String[] steps = new String[panels.length];
        for (int i = 0; i < panels.length; i++) {
            Component c = panels[i].getComponent();
            // Default step name to component name of panel. Mainly useful
            // for getting the name of the target chooser to appear in the
            // list of steps.
            steps[i] = c.getName();
            if (c instanceof JComponent) { // assume Swing components
                JComponent jc = (JComponent) c;
                // Sets step number of a component
                // TODO if using org.openide.dialogs >= 7.8, can use WizardDescriptor.PROP_*:
                jc.putClientProperty("WizardPanel_contentSelectedIndex", new Integer(i));
                // Sets steps names for a panel
                jc.putClientProperty("WizardPanel_contentData", steps);
                // Turn on subtitle creation on each step
                jc.putClientProperty("WizardPanel_autoWizardStyle", Boolean.TRUE);
                // Show steps on the left side with the image on the background
                jc.putClientProperty("WizardPanel_contentDisplayed", Boolean.TRUE);
                // Turn on numbering of all steps
                jc.putClientProperty("WizardPanel_contentNumbered", Boolean.TRUE);
            }
        }

        return panels;
    }

    @Override
    public String getName() {
        return NbBundle.getMessage(CreateTestAction.class, "CreateTestAction.name");
    }

    @Override
    public String iconResource() {
        return null;
    }

    @Override
    public HelpCtx getHelpCtx() {
        return HelpCtx.DEFAULT_HELP;
    }

    @Override
    protected boolean asynchronous() {
        return false;
    }

    @Override
    protected void performAction(Node[] activatedNodes) {
        this.activatedNodes = activatedNodes;
        FileObject file = activatedNodes[0].getLookup().lookup(DataObject.class).getPrimaryFile();
        try {
            while (!file.isFolder()) {
                file = file.getParent();
            }
            while ((project = ProjectManager.getDefault().findProject(file)) == null) {
                file = file.getParent();
            }
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        } catch (IllegalArgumentException ex) {
            Exceptions.printStackTrace(ex);
        }

        wizard = new WizardDescriptor(getPanels());
        // {0} will be replaced by WizardDesriptor.Panel.getComponent().getName()
        wizard.setTitleFormat(new MessageFormat("{0}"));
        wizard.setTitle(NbBundle.getMessage(CreateTestAction.class, "CreateTestAction.wizard.name"));
        Dialog dialog = DialogDisplayer.getDefault().createDialog(wizard);
        dialog.setVisible(true);
        dialog.toFront();
        boolean cancelled = wizard.getValue() != WizardDescriptor.FINISH_OPTION;
        if (!cancelled) {
            try {
                instantiate();
            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
    }

    @Override
    protected boolean enable(Node[] nodes) {
        if (nodes.length == 0) {
            return false;
        }

        /*
         * In most cases, there is just one node selected - that is why
         * this case is handled in a special, more effective way
         * (no collections and iterators created).
         */
        if (nodes.length == 1) {
            final Node node = nodes[0];
            DataObject dataObj;
            FileObject fileObj;
            Project project2;
            if (((dataObj = node.getCookie(DataObject.class)) != null) 
                    && ((fileObj = dataObj.getPrimaryFile()) != null)
                    && fileObj.isValid()
                    && ((project2 = FileOwnerQuery.getOwner(fileObj)) != null)
                    && (getSourceGroup(fileObj, project2) != null)
                    && ("java".equalsIgnoreCase(fileObj.getExt())
                    || (node.getCookie(DataFolder.class) != null))) {

                JUnitPlugin plugin = TestUtil.getPluginForProject(project2);
                return JUnitPluginTrampoline.DEFAULT.canCreateTests(plugin,
                        fileObj);
            } else {
                return false;
            }
        }

        final Collection<FileObject> fileObjs = new ArrayList<FileObject>(nodes.length);
        Project theProject = null;
        boolean result = false;
        for (Node node : nodes) {
            DataObject dataObj = node.getCookie(DataObject.class);
            if (dataObj != null) {
                FileObject fileObj = dataObj.getPrimaryFile();
                if ((fileObj == null) || !fileObj.isValid()) {
                    continue;
                }

                fileObjs.add(fileObj);

                Project prj = FileOwnerQuery.getOwner(fileObj);
                if (prj != null) {
                    if (theProject == null) {
                        theProject = prj;
                    }
                    if (prj != theProject) {
                        return false;        /* files from different projects */
                    }

                    if ((getSourceGroup(fileObj, prj) != null) 
                            && ("java".equalsIgnoreCase(fileObj.getExt())
                            || (node.getCookie(DataFolder.class) != null))) {
                        result = true;
                    }
                }
            }
        }

        if (theProject != null) {
            JUnitPlugin plugin = TestUtil.getPluginForProject(theProject);
            result &= JUnitPluginTrampoline.DEFAULT.canCreateTests(
                    plugin,
                    fileObjs.toArray(new FileObject[fileObjs.size()]));
        }

        return result;
    }

    private void instantiate() throws IOException {
        FileObject testDir = project.getProjectDirectory().getFileObject("test");
        if (testDir == null) {
            // Maven project
            testDir = project.getProjectDirectory().getFileObject("src/test/java");
        }
        Set<FileObject> files = new HashSet<FileObject>();

        if (activatedNodes.length == 1) {
            FileObject sourceDir = getDirectoryWithPackage("src");
            // get the class
            String targetName = (String) wizard.getProperty(SimpleTestVisualPanel.CLASS_TO_TEST);
            if (targetName == null) {
                targetName = "DbUnitTest";
            } else {
                targetName = targetName.substring(targetName.lastIndexOf(".") + 1); // remove package
            }

            files.add(sourceDir.getFileObject(targetName + ".java"));
        }

        if (activatedNodes.length > 1) {
            for (Node node : activatedNodes) {
                files.add(node.getLookup().lookup(DataObject.class).getPrimaryFile());
            }
        }

        JUnitPlugin plugin = TestUtil.getPluginForProject(project);
        if (!JUnitPluginTrampoline.DEFAULT.createTestActionCalled(
                plugin,
                files.toArray(new FileObject[files.size()]))) {
            return;
        }
        Map<CreateTestParam, Object> params = TestUtil.getSettingsMap(false);

        final FileObject[] testFileObjects = JUnitPluginTrampoline.DEFAULT.createTests(
                plugin,
                files.toArray(new FileObject[files.size()]),
                testDir,
                params);

        //XXX: What if the selected class is not testable?
        //     It should not be skipped!

        if (testFileObjects == null) {
            throw new IOException();
        }

        final Set<DataObject> dataObjects = new HashSet<DataObject>((int) (testFileObjects.length * 1.5f));
        for (FileObject testFile : testFileObjects) {
            try {
                dataObjects.add(DataObject.find(testFile));
            } catch (DataObjectNotFoundException ex) {
                //XXX - does nothing special - just continues
            }
        }

        if (dataObjects.isEmpty()) {
            throw new IOException();
        }

        // add DbUnit's jar to classpath
        Library[] libs = new Library[] {LibraryManager.getDefault().getLibrary("DbUnit")};

        if (libs != null) {
            ProjectClassPathModifier.addLibraries(
                    libs,
                    dataObjects.iterator().next().getPrimaryFile(),
                    ClassPath.COMPILE);
        }


        Instantiator inst = new Instantiator(dataObjects);
        new Thread(inst).start();

    }

    /**
     * 
     * @param type 'src' or 'test'
     * @return a FileObject representing a directory where classes are
     */
    private FileObject getDirectoryWithPackage(String type) throws IOException {
        String pckg = (String) wizard.getProperty(SimpleTestVisualPanel.PACKAGE);
        pckg = pckg.replace(".", "/");   // convert a.b.c.d -> a/b/c/d

        FileObject dir = null;
        if ("src".equals(type)) {
            // Maven project
            dir = project.getProjectDirectory().getFileObject("src/main/java");
            // Java Web project
            if (dir == null) {
                dir = project.getProjectDirectory().getFileObject("src/java");
            }
            // other projects, e.g. Java SE
            if (dir == null) {
                dir = project.getProjectDirectory().getFileObject("src");
            }
        }

        if ("test".equals(type)) {
            // Maven project
            dir = project.getProjectDirectory().getFileObject("src/test/java");
            // other projects
            if (dir == null) {
                project.getProjectDirectory().getFileObject("test");
            }
        }

        if (dir.getFileObject(pckg) != null) {
            dir = dir.getFileObject(pckg);
        } else {
            String tmp = pckg;
            while (pckg.length() > 0) {
                if (pckg.contains("/")) {
                    tmp = pckg.substring(0, pckg.indexOf("/"));
                    dir.createFolder(tmp);
                    pckg = pckg.substring(pckg.indexOf("/") + 1);
                } else {
                    tmp = pckg;
                    dir.createFolder(pckg);
                    pckg = "";
                }
                dir = dir.getFileObject(tmp);
            }
        }
        return dir;
    }

    private void addMethods(FileObject classToModify) throws IOException {
        JavaSource classToModifySource = JavaSource.forFileObject(classToModify);

        // a DbUnit test has to extend DatabaseTestCase
        classToModifySource.runModificationTask(
                new AddExtendsTask()).commit();
        classToModifySource.runModificationTask(
                new AddImportTask("org.dbunit.DatabaseTestCase")).commit();

        // a DbUnit test has to override getDataSet() method
        classToModifySource.runModificationTask(
                new GetDataSetTask(wizard)).commit();
        classToModifySource.runModificationTask(
                new AddImportTask("org.dbunit.dataset.xml.FlatXmlDataSet")).commit();
        classToModifySource.runModificationTask(
                new AddImportTask("java.io.FileInputStream")).commit();

        // a DbUnit test has to override getConnection() method
        classToModifySource.runModificationTask(
                new GetConnectionTask(wizard, getConnectionType(false))).commit();
        classToModifySource.runModificationTask(
                new AddImportTask(getConnectionType(true))).commit();
        classToModifySource.runModificationTask(
                new AddImportTask("java.sql.Connection")).commit();
        classToModifySource.runModificationTask(
                new AddImportTask("java.sql.DriverManager")).commit();

        if (wizard.getProperty(DbUnitVisualPanel.SET_UP_OPERATION) != null) {
            classToModifySource.runModificationTask(
                    new GetDatabaseOperationTask(wizard, 0)).commit();
        }

        if (wizard.getProperty(DbUnitVisualPanel.TEAR_DOWN_OPERATION) != null) {
            classToModifySource.runModificationTask(
                    new GetDatabaseOperationTask(wizard, 1)).commit();
        }

        Boolean generate = (Boolean) wizard.getProperty(DbUnitVisualPanel.CLOSE_CONNECTION);
        if (generate != null && generate.booleanValue()) {
            classToModifySource.runModificationTask(
                    new CloseConnectionTask(wizard)).commit();
        }

        generate = (Boolean) wizard.getProperty(DbUnitVisualPanel.GET_DATABASE_TESTER);
        if (generate != null && generate.booleanValue()) {
            classToModifySource.runModificationTask(
                    new GetDatabaseTesterTask(wizard)).commit();
        }

        generate = (Boolean) wizard.getProperty(DbUnitVisualPanel.NEW_DATABASE_TESTER);
        if (generate != null && generate.booleanValue()) {
            classToModifySource.runModificationTask(
                    new NewDatabaseTesterTask(wizard)).commit();
        }
    }

    /**
     * Get connection type from database connection url.
     * E.g. for URL 'jdbc:mysql://localhost:3306/test' it will return MySqlConnection
     * or org.dbunit.ext.mysql.MySqlConnection.
     * @param fqn if it should return fully qualified class name
     * @return connection type
     */
    private String getConnectionType(boolean fqn) {
        String type = (String) wizard.getProperty(DbUnitVisualPanel.DATABASE_URL);
        if (type == null || type.length() == 0) {
            return fqn ? "org.dbunit.database.DatabaseConnection" : "DatabaseConnection";
        }
        type = type.split(":")[1];

        if (type.equals("mysql")) {
            return fqn ? "org.dbunit.ext.mysql.MySqlConnection" : "MySqlConnection";
        }

        if (type.equals("db2")) {
            return fqn ? "org.dbunit.ext.db2.Db2Connection" : "org.dbunit.ext.db2.Db2Connection";
        }

        if (type.equals("h2")) {
            return fqn ? "org.dbunit.ext.h2.H2Connection" : "H2Connection";
        }

        if (type.equals("hsqldb")) {
            return fqn ? "org.dbunit.ext.hsqldb.HsqldbConnection" : "HsqldbConnection";
        }

        if (type.equals("mssql")) {
            return fqn ? "org.dbunit.ext.mssql.MsSqlConnection" : "MsSqlConnection";
        }

        if (type.equals("oracle")) {
            return fqn ? "org.dbunit.ext.oracle.OracleConnection" : "OracleConnection";
        }

        return fqn ? "org.dbunit.database.DatabaseConnection" : "DatabaseConnection";
    }

    private class Instantiator implements Runnable {

        private Set<DataObject> dataObjects;

        public Instantiator(Set<DataObject> dataObjects) {
            this.dataObjects = dataObjects;
        }

        @Override
        public void run() {
            DataObject file = null;
            OpenCookie openCookie = null;
            Iterator<DataObject> it = dataObjects.iterator();

            while (it.hasNext()) {
                file = it.next();
                StatusDisplayer.getDefault().setStatusText(NbBundle.getMessage(
                        Instantiator.class,
                        "CreateTestAction.Instantiator.message.creatingTest",
                        file.getPrimaryFile().getName()));
                try {
                    addMethods(file.getPrimaryFile());
                } catch (IOException ex) {
                    Exceptions.printStackTrace(ex);
                }
                openCookie = file.getCookie(OpenCookie.class);
                openCookie.open();
            }

        }
    }

    private SourceGroup getSourceGroup(FileObject file, Project prj) {
        Sources src = ProjectUtils.getSources(prj);
        SourceGroup[] srcGrps = src.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA);
        for (SourceGroup srcGrp : srcGrps) {
            FileObject rootFolder = srcGrp.getRootFolder();
            if (((file == rootFolder) || FileUtil.isParentOf(rootFolder, file)) && srcGrp.contains(file)) {
                return srcGrp;
            }
        }
        return null;
    }

    private String buildClassName(Node node) {
        if (node.isLeaf()) {
            return buildClassName(node.getParentNode()) + "." + node.getDisplayName();
        } else {
            return node.getDisplayName();
        }
    }
    
}
